pub use super::_entities::admins::{self, ActiveModel, Entity, Model};
pub(crate) use crate::models::_entities::admins::CreateAdminParams;
use loco_rs::{hash, prelude::*};
use serde::Deserialize;
use uuid::Uuid;

#[derive(Debug, Validate, Deserialize)]
pub struct Validator {
    #[validate(length(min = 2, message = "Name must be at least 2 characters long."))]
    pub name: String,
    #[validate(custom(function = "validation::is_valid_email"))]
    pub email: String,
}

impl Validatable for ActiveModel {
    fn validator(&self) -> Box<dyn Validate> {
        Box::new(Validator {
            name: self.name.as_ref().to_owned(),
            email: self.email.as_ref().to_owned(),
        })
    }
}

#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {
    async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
    where
        C: ConnectionTrait,
    {
        self.validate()?;
        if insert {
            let mut this = self;
            this.id = Set(Uuid::new_v4().to_string());
            Ok(this)
        } else {
            Ok(self)
        }
    }
}

impl Model {
    /// finds an admin by the provided email
    ///
    /// # Errors
    ///
    /// When could not find admin by the given email or DB query error
    pub async fn find_by_email(db: &DatabaseConnection, email: &str) -> ModelResult<Self> {
        let admin = admins::Entity::find()
            .filter(
                model::query::condition()
                    .eq(admins::Column::Email, email)
                    .build(),
            )
            .one(db)
            .await?;
        admin.ok_or_else(|| ModelError::EntityNotFound)
    }

    /// finds an admin by the provided id
    ///
    /// # Errors
    ///
    /// When could not find admin or DB query error
    pub async fn find_by_id(db: &DatabaseConnection, id: &str) -> ModelResult<Self> {
        let admin = admins::Entity::find()
            .filter(model::query::condition().eq(admins::Column::Id, id).build())
            .one(db)
            .await?;
        admin.ok_or_else(|| ModelError::EntityNotFound)
    }

    /// Verifies whether the provided plain password matches the hashed password
    ///
    /// # Errors
    ///
    /// when could not verify password
    #[must_use]
    pub fn verify_password(&self, password: &str) -> bool {
        hash::verify_password(password, &self.password)
    }

    /// Asynchronously creates an admin with a password and saves it to the
    /// database.
    ///
    /// # Errors
    ///
    /// When could not save the admin into the DB
    pub async fn create_with_password(
        db: &DatabaseConnection,
        params: &CreateAdminParams,
    ) -> ModelResult<Self> {
        let txn = db.begin().await?;

        if admins::Entity::find()
            .filter(
                query::condition()
                    .eq(admins::Column::Email, &params.email)
                    .build(),
            )
            .one(&txn)
            .await?
            .is_some()
        {
            return Err(ModelError::EntityAlreadyExists {});
        }

        let password_hash =
            hash::hash_password(&params.password).map_err(|e| ModelError::Any(e.into()))?;
        let admin = admins::ActiveModel {
            email: ActiveValue::set(params.email.to_string()),
            password: ActiveValue::set(password_hash),
            name: ActiveValue::set(params.name.to_string()),
            locale: ActiveValue::set(params.locale.to_string()),
            active: ActiveValue::set(true),
            ..Default::default()
        }
        .insert(&txn)
        .await?;

        txn.commit().await?;

        Ok(admin)
    }
}

impl ActiveModel {
    /// Updates the admin's password with a new one
    ///
    /// # Errors
    ///
    /// when has DB query error or could not hash the given password
    pub async fn update_password(
        mut self,
        db: &DatabaseConnection,
        password: &str,
    ) -> ModelResult<Model> {
        self.password =
            ActiveValue::set(hash::hash_password(password).map_err(|e| ModelError::Any(e.into()))?);
        Ok(self.update(db).await?)
    }
}
